const passport = require('passport-strategy');
const util = require('util');
const chalk = require('chalk');
const CryptoJS = require('crypto-js');
const randtoken = require('rand-token');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const atob = require('atob');
const fetch = require('node-fetch');
const profile = require('./profile');

const print = (string) => {
  if (process.env.DISABLE_AUTH_LOGS !== 'true') {
    console.log(`${chalk.bgWhite.bold('[AUTH]')} : ${string}`);
  }
};

function Strategy(options, verify) {
  if (!verify) {
    throw new TypeError('CustomStrategy requires a verify callback');
  }
  passport.Strategy.call(this);
  this.name = 'custom';
  this.issuer = options.issuer;
  this.callbackPath = options.callbackPath;
  this.clientID = options.clientID;
  this.clientSecret = options.clientSecret;
  this.scope = options.scope;
  this.verify = verify;
}

/**
* Inherit from `passport.Strategy`.
*/
util.inherits(Strategy, passport.Strategy);

Strategy.prototype.authenticate = function (req, options) { // eslint-disable-line
  const self = this;

  if (req.path === self.callbackPath) {
    req.session.id_token = req.body.id_token;
  }

  if (req.session.id_token) {
    // check if the client sent an id token
    print(chalk.bgCyan("Verifying user's token..."));
    // attempt to verify the token
    self.verifyToken(req)
      .then(([isVerified, result]) => {
        // if the token is verified successfully
        if (isVerified) {
          print(chalk.bgGreen('User successfully verified.'));
          print(chalk.bgGreen(`Access to ${req.originalUrl} granted!`));
          const user = { id: result.sub };
          return self.verify(req, result.iss, result.sub, user,
              'jwtClaims', 'accessToken', 'refreshToken',
              'params', verified);
        } else {
          print(chalk.bgYellow('User\'s token is unverified!'));
          self.redirectToAuth(req);
        }
      })
      .catch((ex) => {
        self.error(ex);
      });
  } else {
    self.redirectToAuth(req);
  }

  function verified(err, user, info) {
    if (err) {
      self.error(err);
      return;
    }
    if (!user) {
      self.fail(info);
      return;
    }
    self.success(user, info);
  }
};

Strategy.prototype.verifyToken = function verifyToken(req) {
  return new Promise((resolve) => {
    // fetch our JWKS config to convert to public key
    fetch(`${this.issuer}/pf/JWKS`, {
      method: 'GET',
      body: 'a=1',
    }).then(resp => resp.json()).then((json) => {
      // decode our token to extract the kid to match the signature key
      const decoded = JSON.parse(atob(req.session.id_token.split('.')[0]));
      // check the response from JWKS config url,
      // attempt to match the kid from the token to a signature
      json.keys.forEach((key) => {
        // if this signature key's kid matches our decoded token's kid
        if (key.kid === decoded.kid) {
          // convert the JWKS signature key to a pem
          const pem = jwkToPem(key);

          // verify the token signature with the signature we matched from JWKS
          jwt.verify(req.session.id_token, pem, {
            algorithms: 'RS256',
            ignoreExpiration: true,
          }, (err, result) => {
            if (!req.session.nonce) {
              resolve([false]);
              return;
            }

            console.log(req.session.nonce.toString());
            console.log(result.nonce.toString());
            const HMAC = CryptoJS.HmacSHA256(req.session.nonce.toString(), process.env.OPENID_NONCE_SECRET);
            const HMAC_JWT = CryptoJS.enc.Base64.stringify(HMAC);
            console.log(HMAC_JWT);
            // if there's a result from verification...
            if (result &&
              // Check that all the JWT verified info matches our configurated info
              decodeURIComponent(result.nonce.toString()) === HMAC_JWT &&
              result.aud === this.clientID &&
              result.iss === this.issuer) {
              // if everything matches, authenticate the user
              resolve([true, result]);
            } else {
              resolve([false]);
            }
          });
        }
      });
    }).catch((err) => {
      console.log(err);
    });
  });
};

Strategy.prototype.redirectToAuth = function redirectToAuth(req) {
  const self = this;
  self.getAuthCallbackURL(req).then((callback) => {
    const secret = process.env.OPENID_NONCE_SECRET;
    const nonce = CryptoJS.AES.encrypt(randtoken.generate(16), secret);
    if (!req.session.nonce) {
      req.session.nonce = nonce.toString();
    }
    const HMAC = CryptoJS.HmacSHA256(nonce.toString(), secret);
    // this is the nonce parameter (crypto hash)
    const HMACB64 = CryptoJS.enc.Base64.stringify(HMAC);
    console.log(`HMACB64: ${HMACB64}`);
    // attempt to authenticate the user by using OpenID flow
    self.redirect(`${self.issuer}/as/authorization.oauth2`
      + `?scope=${self.scope}`
      + `&client_id=${self.clientID}`
      + '&response_type=id_token'
      + `&nonce=${encodeURIComponent(HMACB64)}`
      + `&state=${encodeURIComponent(req.originalUrl)}`
      + `&redirect_uri=${encodeURIComponent(callback)}`);
  }).catch((ex) => {
    self.error(ex);
  });
};

Strategy.prototype.getAuthCallbackURL = function getAuthCallbackURL(req) {
  return new Promise((resolve, reject) => {
    let callbackURL;
    if (process.env.NODE_ENV === 'production') {
      callbackURL = `${req.get('X-Forwarded-Proto')}://${req.get('host')}${process.env.OPENID_CALLBACK}`;
      resolve(callbackURL);
    } else if (process.env.NODE_ENV === 'development') {
      callbackURL = `https://${req.get('host')}${process.env.OPENID_CALLBACK}`;
      resolve(callbackURL);
    } else {
      reject(callbackURL);
    }
  });
};

/**
* Expose `Strategy`.
*/
module.exports = Strategy;
